package org.apache.solr.schema;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.Field.TermVector;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.solr.common.ResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.response.XMLWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SolrConstantScoreQuery;
import org.apache.solr.search.function.DocValues;
import org.apache.solr.search.function.ValueSource;
import org.apache.solr.search.function.ValueSourceRangeFilter;
import org.apache.solr.util.plugin.ResourceLoaderAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Field type for support of monetary values.
 * <p>
 * See <a
 * href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org
 * /solr/CurrencyField</a>
 */
public class CurrencyField extends FieldType implements SchemaAware,
		ResourceLoaderAware {
	protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency";
	protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass";
	protected static final Object PARAM_PRECISION_STEP = "precisionStep";
	protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider";
	protected static final String DEFAULT_DEFAULT_CURRENCY = "USD";
	protected static final String DEFAULT_PRECISION_STEP = "0";
	protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw";
	protected static final String FIELD_SUFFIX_CURRENCY = "_currency";

	private IndexSchema schema;
	protected FieldType fieldTypeCurrency;
	protected FieldType fieldTypeAmountRaw;
	private String exchangeRateProviderClass;
	private String defaultCurrency;
	private ExchangeRateProvider provider;
	public static Logger log = LoggerFactory.getLogger(CurrencyField.class);

	@Override
	protected void init(IndexSchema schema, Map<String, String> args) {
		super.init(schema, args);
		this.schema = schema;
		this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS);
		this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY);

		if (this.defaultCurrency == null) {
			this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY;
		}

		if (this.exchangeRateProviderClass == null) {
			this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS;
		}

		if (java.util.Currency.getInstance(this.defaultCurrency) == null) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Invalid currency code " + this.defaultCurrency);
		}

		String precisionStepString = args.get(PARAM_PRECISION_STEP);
		if (precisionStepString == null) {
			precisionStepString = DEFAULT_PRECISION_STEP;
		}

		// Initialize field type for amount
		fieldTypeAmountRaw = new TrieLongField();
		fieldTypeAmountRaw.setTypeName("amount_raw_type_tlong");
		Map<String, String> map = new HashMap<String, String>(1);
		map.put("precisionStep", precisionStepString);
		fieldTypeAmountRaw.init(schema, map);

		// Initialize field type for currency string
		fieldTypeCurrency = new StrField();
		fieldTypeCurrency.setTypeName("currency_type_string");
		fieldTypeCurrency.init(schema, new HashMap<String, String>());

		args.remove(PARAM_RATE_PROVIDER_CLASS);
		args.remove(PARAM_DEFAULT_CURRENCY);
		args.remove(PARAM_PRECISION_STEP);

		try {
			Class<?> c = schema.getResourceLoader().findClass(
					exchangeRateProviderClass);
			Object clazz = c.newInstance();
			if (clazz instanceof ExchangeRateProvider) {
				provider = (ExchangeRateProvider) clazz;
				provider.init(args);
			} else {
				throw new SolrException(ErrorCode.BAD_REQUEST,
						"exchangeRateProvider " + exchangeRateProviderClass
								+ " needs to implement ExchangeRateProvider");
			}
		} catch (Exception e) {
			throw new SolrException(ErrorCode.BAD_REQUEST,
					"Error instansiating exhange rate provider "
							+ exchangeRateProviderClass
							+ ". Please check your FieldType configuration", e);
		}
	}

	@Override
	public boolean isPolyField() {
		return true;
	}

	@Override
	public Fieldable[] createFields(SchemaField field, String externalVal,
			float boost) {
		CurrencyValue value = CurrencyValue.parse(externalVal, defaultCurrency);

		Fieldable[] f = new Fieldable[field.stored() ? 3 : 2];
		SchemaField amountField = getAmountField(field);
		f[0] = amountField.createField(String.valueOf(value.getAmount()),
				amountField.omitNorms() ? 1F : boost);
		SchemaField currencyField = getCurrencyField(field);
		f[1] = currencyField.createField(value.getCurrencyCode(),
				currencyField.omitNorms() ? 1F : boost);

		if (field.stored()) {
			// TODO: funky to omit norms on a stored-only field here!
			String storedValue = externalVal.toString().trim();
			if (storedValue.indexOf(",") < 0) {
				storedValue += "," + defaultCurrency;
			}
			f[2] = createField(field.getName(), storedValue, Store.YES,
					Index.NO, TermVector.NO, true, null, boost);
		}

		return f;
	}

	private SchemaField getAmountField(SchemaField field) {
		return schema.getField(field.getName() + POLY_FIELD_SEPARATOR
				+ FIELD_SUFFIX_AMOUNT_RAW);
	}

	private SchemaField getCurrencyField(SchemaField field) {
		return schema.getField(field.getName() + POLY_FIELD_SEPARATOR
				+ FIELD_SUFFIX_CURRENCY);
	}

	private void createDynamicCurrencyField(String suffix, FieldType type) {
		String name = "*" + POLY_FIELD_SEPARATOR + suffix;
		Map<String, String> props = new HashMap<String, String>();
		props.put("indexed", "true");
		props.put("stored", "false");
		props.put("multiValued", "false");
		props.put("omitNorms", "true");
		int p = SchemaField.calcProps(name, type, props);
		schema.registerDynamicField(SchemaField.create(name, type, p, null));
	}

	/**
	 * When index schema is informed, add dynamic fields.
	 * 
	 * @param indexSchema
	 *            The index schema.
	 */
	public void inform(IndexSchema indexSchema) {
		createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, fieldTypeCurrency);
		createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, fieldTypeAmountRaw);
	}

	/**
	 * Load the currency config when resource loader initialized.
	 * 
	 * @param resourceLoader
	 *            The resource loader.
	 */
	public void inform(ResourceLoader resourceLoader) {
		provider.inform(resourceLoader);
		boolean reloaded = provider.reload();
		if (!reloaded) {
			log.warn("Failed reloading currencies");
		}
	}

	@Override
	public Query getFieldQuery(QParser parser, SchemaField field,
			String externalVal) {
		CurrencyValue value = CurrencyValue.parse(externalVal, defaultCurrency);
		CurrencyValue valueDefault;
		valueDefault = value.convertTo(provider, defaultCurrency);

		return getRangeQuery(parser, field, valueDefault, valueDefault, true,
				true);
	}

	@Override
	public Query getRangeQuery(QParser parser, SchemaField field, String part1,
			String part2, final boolean minInclusive, final boolean maxInclusive) {
		final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency);
		final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency);

		if (!p1.getCurrencyCode().equals(p2.getCurrencyCode())) {
			throw new SolrException(
					SolrException.ErrorCode.BAD_REQUEST,
					"Cannot parse range query "
							+ part1
							+ " to "
							+ part2
							+ ": range queries only supported when upper and lower bound have same currency.");
		}

		return getRangeQuery(parser, field, p1, p2, minInclusive, maxInclusive);
	}

	public Query getRangeQuery(QParser parser, SchemaField field,
			final CurrencyValue p1, final CurrencyValue p2,
			final boolean minInclusive, final boolean maxInclusive) {
		String currencyCode = p1.getCurrencyCode();
		final CurrencyValueSource vs = new CurrencyValueSource(field,
				currencyCode, parser);

		return new SolrConstantScoreQuery(new ValueSourceRangeFilter(vs,
				p1.getAmount() + "", p2.getAmount() + "", minInclusive,
				maxInclusive));
	}

	@Override
	public SortField getSortField(SchemaField field, boolean reverse) {
		try {
			// Convert all values to default currency for sorting.
			return (new CurrencyValueSource(field, defaultCurrency, null))
					.getSortField(reverse);
		} catch (IOException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
		}
	}

	public void write(XMLWriter xmlWriter, String name, Fieldable field)
			throws IOException {
		xmlWriter.writeStr(name, field.stringValue());
	}

	@Override
	public void write(TextResponseWriter writer, String name, Fieldable field)
			throws IOException {
		writer.writeStr(name, field.stringValue(), false);
	}

	public ExchangeRateProvider getProvider() {
		return provider;
	}

	class CurrencyValueSource extends ValueSource {
		private static final long serialVersionUID = 1L;
		private String targetCurrencyCode;
		private ValueSource currencyValues;
		private ValueSource amountValues;
		private final SchemaField sf;

		public CurrencyValueSource(SchemaField sfield,
				String targetCurrencyCode, QParser parser) {
			this.sf = sfield;
			this.targetCurrencyCode = targetCurrencyCode;

			SchemaField amountField = schema.getField(sf.getName()
					+ POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
			SchemaField currencyField = schema.getField(sf.getName()
					+ POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY);

			currencyValues = currencyField.getType().getValueSource(
					currencyField, parser);
			amountValues = amountField.getType().getValueSource(amountField,
					parser);
		}

		public DocValues getValues(Map context, IndexReader reader)
				throws IOException {
			final DocValues amounts = amountValues.getValues(context, reader);
			final DocValues currencies = currencyValues.getValues(context,
					reader);

			return new DocValues() {
				private final int MAX_CURRENCIES_TO_CACHE = 256;
				private final int[] fractionDigitCache = new int[MAX_CURRENCIES_TO_CACHE];
				private final String[] currencyOrdToCurrencyCache = new String[MAX_CURRENCIES_TO_CACHE];
				private final double[] exchangeRateCache = new double[MAX_CURRENCIES_TO_CACHE];
				private int targetFractionDigits = -1;
				private int targetCurrencyOrd = -1;
				private boolean initializedCache;

				private String getDocCurrencyCode(int doc, int currencyOrd) {
					if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
						String currency = currencyOrdToCurrencyCache[currencyOrd];

						if (currency == null) {
							currencyOrdToCurrencyCache[currencyOrd] = currency = currencies
									.strVal(doc);
						}

						if (currency == null) {
							currency = defaultCurrency;
						}

						if (targetCurrencyOrd == -1
								&& currency.equals(targetCurrencyCode)) {
							targetCurrencyOrd = currencyOrd;
						}

						return currency;
					} else {
						return currencies.strVal(doc);
					}
				}

				public long longVal(int doc) {
					if (!initializedCache) {
						for (int i = 0; i < fractionDigitCache.length; i++) {
							fractionDigitCache[i] = -1;
						}

						initializedCache = true;
					}

					long amount = amounts.longVal(doc);
					int currencyOrd = currencies.intVal(doc);

					if (currencyOrd == targetCurrencyOrd) {
						return amount;
					}

					double exchangeRate;
					int sourceFractionDigits;

					if (targetFractionDigits == -1) {
						targetFractionDigits = Currency.getInstance(
								targetCurrencyCode).getDefaultFractionDigits();
					}

					if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
						exchangeRate = exchangeRateCache[currencyOrd];

						if (exchangeRate <= 0.0) {
							String sourceCurrencyCode = getDocCurrencyCode(doc,
									currencyOrd);
							exchangeRate = exchangeRateCache[currencyOrd] = provider
									.getExchangeRate(sourceCurrencyCode,
											targetCurrencyCode);
						}

						sourceFractionDigits = fractionDigitCache[currencyOrd];

						if (sourceFractionDigits == -1) {
							String sourceCurrencyCode = getDocCurrencyCode(doc,
									currencyOrd);
							sourceFractionDigits = fractionDigitCache[currencyOrd] = Currency
									.getInstance(sourceCurrencyCode)
									.getDefaultFractionDigits();
						}
					} else {
						String sourceCurrencyCode = getDocCurrencyCode(doc,
								currencyOrd);
						exchangeRate = provider.getExchangeRate(
								sourceCurrencyCode, targetCurrencyCode);
						sourceFractionDigits = Currency.getInstance(
								sourceCurrencyCode).getDefaultFractionDigits();
					}

					return CurrencyValue.convertAmount(exchangeRate,
							sourceFractionDigits, amount, targetFractionDigits);
				}

				public int intVal(int doc) {
					return (int) longVal(doc);
				}

				public double doubleVal(int doc) {
					return (double) longVal(doc);
				}

				public float floatVal(int doc) {
					return (float) longVal(doc);
				}

				public String strVal(int doc) {
					return Long.toString(longVal(doc));
				}

				public String toString(int doc) {
					return name() + '(' + amounts.toString(doc) + ','
							+ currencies.toString(doc) + ')';
				}
			};
		}

		public String name() {
			return "currency";
		}

		@Override
		public String description() {
			return name() + "(" + sf.getName() + ")";
		}

		@Override
		public boolean equals(Object o) {
			if (this == o)
				return true;
			if (o == null || getClass() != o.getClass())
				return false;

			CurrencyValueSource that = (CurrencyValueSource) o;

			return !(amountValues != null ? !amountValues
					.equals(that.amountValues) : that.amountValues != null)
					&& !(currencyValues != null ? !currencyValues
							.equals(that.currencyValues)
							: that.currencyValues != null)
					&& !(targetCurrencyCode != null ? !targetCurrencyCode
							.equals(that.targetCurrencyCode)
							: that.targetCurrencyCode != null);

		}

		@Override
		public int hashCode() {
			int result = targetCurrencyCode != null ? targetCurrencyCode
					.hashCode() : 0;
			result = 31 * result
					+ (currencyValues != null ? currencyValues.hashCode() : 0);
			result = 31 * result
					+ (amountValues != null ? amountValues.hashCode() : 0);
			return result;
		}
	}
}

/**
 * Configuration for currency. Provides currency exchange rates.
 */
class FileExchangeRateProvider implements ExchangeRateProvider {
	public static Logger log = LoggerFactory
			.getLogger(FileExchangeRateProvider.class);
	protected static final String PARAM_CURRENCY_CONFIG = "currencyConfig";

	// Exchange rate map, maps Currency Code -> Currency Code -> Rate
	private Map<String, Map<String, Double>> rates = new HashMap<String, Map<String, Double>>();

	private String currencyConfigFile;
	private ResourceLoader loader;

	/**
	 * Returns the currently known exchange rate between two currencies. If a
	 * direct rate has been loaded, it is used. Otherwise, if a rate is known to
	 * convert the target currency to the source, the inverse exchange rate is
	 * computed.
	 * 
	 * @param sourceCurrencyCode
	 *            The source currency being converted from.
	 * @param targetCurrencyCode
	 *            The target currency being converted to.
	 * @return The exchange rate.
	 * @throws an
	 *             exception if the requested currency pair cannot be found
	 */
	public double getExchangeRate(String sourceCurrencyCode,
			String targetCurrencyCode) {
		if (sourceCurrencyCode == null || targetCurrencyCode == null) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Cannot get exchange rate; currency was null.");
		}

		if (sourceCurrencyCode.equals(targetCurrencyCode)) {
			return 1.0;
		}

		Double directRate = lookupRate(sourceCurrencyCode, targetCurrencyCode);

		if (directRate != null) {
			return directRate;
		}

		Double symmetricRate = lookupRate(targetCurrencyCode,
				sourceCurrencyCode);

		if (symmetricRate != null) {
			return 1.0 / symmetricRate;
		}

		throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
				"No available conversion rate between " + sourceCurrencyCode
						+ " to " + targetCurrencyCode);
	}

	/**
	 * Looks up the current known rate, if any, between the source and target
	 * currencies.
	 * 
	 * @param sourceCurrencyCode
	 *            The source currency being converted from.
	 * @param targetCurrencyCode
	 *            The target currency being converted to.
	 * @return The exchange rate, or null if no rate has been registered.
	 */
	private Double lookupRate(String sourceCurrencyCode,
			String targetCurrencyCode) {
		Map<String, Double> rhs = rates.get(sourceCurrencyCode);

		if (rhs != null) {
			return rhs.get(targetCurrencyCode);
		}

		return null;
	}

	/**
	 * Registers the specified exchange rate.
	 * 
	 * @param ratesMap
	 *            The map to add rate to
	 * @param sourceCurrencyCode
	 *            The source currency.
	 * @param targetCurrencyCode
	 *            The target currency.
	 * @param rate
	 *            The known exchange rate.
	 */
	private void addRate(Map<String, Map<String, Double>> ratesMap,
			String sourceCurrencyCode, String targetCurrencyCode, double rate) {
		Map<String, Double> rhs = ratesMap.get(sourceCurrencyCode);

		if (rhs == null) {
			rhs = new HashMap<String, Double>();
			ratesMap.put(sourceCurrencyCode, rhs);
		}

		rhs.put(targetCurrencyCode, rate);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;

		FileExchangeRateProvider that = (FileExchangeRateProvider) o;

		return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
	}

	@Override
	public int hashCode() {
		return rates != null ? rates.hashCode() : 0;
	}

	public String toString() {
		return "[" + this.getClass().getName() + " : " + rates.size()
				+ " rates.]";
	}

	public Set<String> listAvailableCurrencies() {
		Set<String> currencies = new HashSet<String>();
		for (String from : rates.keySet()) {
			currencies.add(from);
			for (String to : rates.get(from).keySet()) {
				currencies.add(to);
			}
		}
		return currencies;
	}

	public boolean reload() throws SolrException {
		InputStream is = null;
		Map<String, Map<String, Double>> tmpRates = new HashMap<String, Map<String, Double>>();
		try {
			log.info("Reloading exchange rates from file "
					+ this.currencyConfigFile);

			is = loader.openResource(currencyConfigFile);
			javax.xml.parsers.DocumentBuilderFactory dbf = DocumentBuilderFactory
					.newInstance();
			try {
				dbf.setXIncludeAware(true);
				dbf.setNamespaceAware(true);
			} catch (UnsupportedOperationException e) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"XML parser doesn't support XInclude option", e);
			}

			try {
				Document doc = dbf.newDocumentBuilder().parse(is);
				XPathFactory xpathFactory = XPathFactory.newInstance();
				XPath xpath = xpathFactory.newXPath();

				// Parse exchange rates.
				NodeList nodes = (NodeList) xpath.evaluate(
						"/currencyConfig/rates/rate", doc,
						XPathConstants.NODESET);

				for (int i = 0; i < nodes.getLength(); i++) {
					Node rateNode = nodes.item(i);
					NamedNodeMap attributes = rateNode.getAttributes();
					Node from = attributes.getNamedItem("from");
					Node to = attributes.getNamedItem("to");
					Node rate = attributes.getNamedItem("rate");

					if (from == null || to == null || rate == null) {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"Exchange rate missing attributes (required: from, to, rate) "
										+ rateNode);
					}

					String fromCurrency = from.getNodeValue();
					String toCurrency = to.getNodeValue();
					Double exchangeRate;

					if (java.util.Currency.getInstance(fromCurrency) == null
							|| java.util.Currency.getInstance(toCurrency) == null) {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"Could not find from currency specified in exchange rate: "
										+ rateNode);
					}

					try {
						exchangeRate = Double.parseDouble(rate.getNodeValue());
					} catch (NumberFormatException e) {
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"Could not parse exchange rate: " + rateNode, e);
					}

					addRate(tmpRates, fromCurrency, toCurrency, exchangeRate);
				}
			} catch (SAXException e) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Error parsing currency config.", e);
			} catch (IOException e) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Error parsing currency config.", e);
			} catch (ParserConfigurationException e) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Error parsing currency config.", e);
			} catch (XPathExpressionException e) {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Error parsing currency config.", e);
			}
		} catch (IOException e) {
			throw new SolrException(ErrorCode.BAD_REQUEST,
					"Error while opening Currency configuration file "
							+ currencyConfigFile, e);
		} finally {
			try {
				if (is != null) {
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		// Atomically swap in the new rates map, if it loaded successfully
		this.rates = tmpRates;
		return true;
	}

	public void init(Map<String, String> params) throws SolrException {
		this.currencyConfigFile = params.get(PARAM_CURRENCY_CONFIG);
		if (currencyConfigFile == null) {
			throw new SolrException(ErrorCode.NOT_FOUND,
					"Missing required configuration " + PARAM_CURRENCY_CONFIG);
		}

		// Removing config params custom to us
		params.remove(PARAM_CURRENCY_CONFIG);
	}

	public void inform(ResourceLoader loader) throws SolrException {
		if (loader == null) {
			throw new SolrException(ErrorCode.BAD_REQUEST,
					"Needs ResourceLoader in order to load config file");
		}
		this.loader = loader;
		reload();
	}
}

/**
 * Represents a Currency field value, which includes a long amount and ISO
 * currency code.
 */
class CurrencyValue {
	private long amount;
	private String currencyCode;

	/**
	 * Constructs a new currency value.
	 * 
	 * @param amount
	 *            The amount.
	 * @param currencyCode
	 *            The currency code.
	 */
	public CurrencyValue(long amount, String currencyCode) {
		this.amount = amount;
		this.currencyCode = currencyCode;
	}

	/**
	 * Constructs a new currency value by parsing the specific input.
	 * <p/>
	 * Currency values are expected to be in the format
	 * &lt;amount&gt;,&lt;currency code&gt;, for example, "500,USD" would
	 * represent 5 U.S. Dollars.
	 * <p/>
	 * If no currency code is specified, the default is assumed.
	 * 
	 * @param externalVal
	 *            The value to parse.
	 * @param defaultCurrency
	 *            The default currency.
	 * @return The parsed CurrencyValue.
	 */
	public static CurrencyValue parse(String externalVal, String defaultCurrency) {
		String amount = externalVal;
		String code = defaultCurrency;

		if (externalVal.contains(",")) {
			String[] amountAndCode = externalVal.split(",");
			amount = amountAndCode[0];
			code = amountAndCode[1];
		}

		Currency currency = java.util.Currency.getInstance(code);

		if (currency == null) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Invalid currency code " + code);
		}

		try {
			double value = Double.parseDouble(amount);
			long currencyValue = Math.round(value
					* Math.pow(10.0, currency.getDefaultFractionDigits()));

			return new CurrencyValue(currencyValue, code);
		} catch (NumberFormatException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
		}
	}

	/**
	 * The amount of the CurrencyValue.
	 * 
	 * @return The amount.
	 */
	public long getAmount() {
		return amount;
	}

	/**
	 * The ISO currency code of the CurrencyValue.
	 * 
	 * @return The currency code.
	 */
	public String getCurrencyCode() {
		return currencyCode;
	}

	/**
	 * Performs a currency conversion & unit conversion.
	 * 
	 * @param exchangeRates
	 *            Exchange rates to apply.
	 * @param sourceCurrencyCode
	 *            The source currency code.
	 * @param sourceAmount
	 *            The source amount.
	 * @param targetCurrencyCode
	 *            The target currency code.
	 * @return The converted indexable units after the exchange rate and
	 *         currency fraction digits are applied.
	 */
	public static long convertAmount(ExchangeRateProvider exchangeRates,
			String sourceCurrencyCode, long sourceAmount,
			String targetCurrencyCode) {
		double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode,
				targetCurrencyCode);
		return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount,
				targetCurrencyCode);
	}

	/**
	 * Performs a currency conversion & unit conversion.
	 * 
	 * @param exchangeRate
	 *            Exchange rate to apply.
	 * @param sourceFractionDigits
	 *            The fraction digits of the source.
	 * @param sourceAmount
	 *            The source amount.
	 * @param targetFractionDigits
	 *            The fraction digits of the target.
	 * @return The converted indexable units after the exchange rate and
	 *         currency fraction digits are applied.
	 */
	public static long convertAmount(final double exchangeRate,
			final int sourceFractionDigits, final long sourceAmount,
			final int targetFractionDigits) {
		int digitDelta = targetFractionDigits - sourceFractionDigits;
		double value = ((double) sourceAmount * exchangeRate);

		if (digitDelta != 0) {
			if (digitDelta < 0) {
				for (int i = 0; i < -digitDelta; i++) {
					value *= 0.1;
				}
			} else {
				for (int i = 0; i < digitDelta; i++) {
					value *= 10.0;
				}
			}
		}

		return (long) value;
	}

	/**
	 * Performs a currency conversion & unit conversion.
	 * 
	 * @param exchangeRate
	 *            Exchange rate to apply.
	 * @param sourceCurrencyCode
	 *            The source currency code.
	 * @param sourceAmount
	 *            The source amount.
	 * @param targetCurrencyCode
	 *            The target currency code.
	 * @return The converted indexable units after the exchange rate and
	 *         currency fraction digits are applied.
	 */
	public static long convertAmount(double exchangeRate,
			String sourceCurrencyCode, long sourceAmount,
			String targetCurrencyCode) {
		if (targetCurrencyCode.equals(sourceCurrencyCode)) {
			return sourceAmount;
		}

		int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode)
				.getDefaultFractionDigits();
		Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
		int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
		return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount,
				targetFractionDigits);
	}

	/**
	 * Returns a new CurrencyValue that is the conversion of this CurrencyValue
	 * to the specified currency.
	 * 
	 * @param exchangeRates
	 *            The exchange rate provider.
	 * @param targetCurrencyCode
	 *            The target currency code to convert this CurrencyValue to.
	 * @return The converted CurrencyValue.
	 */
	public CurrencyValue convertTo(ExchangeRateProvider exchangeRates,
			String targetCurrencyCode) {
		return new CurrencyValue(convertAmount(exchangeRates,
				this.getCurrencyCode(), this.getAmount(), targetCurrencyCode),
				targetCurrencyCode);
	}

	public String toString() {
		return String.valueOf(amount) + "," + currencyCode;
	}
}